#!/usr/bin/env python

# Simple770, Copyright 2008, Roy Wood

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import sys
import os
import select
import logging
import optparse
import signal
import time
import pygtk
import gobject
import gtk
import gtk.glade


pygtk.require('2.0')

myVersion = "0.9.8"


# Show a message dialog
def runMessageDialog(message, dialogType=gtk.MESSAGE_INFO, dialogButtons=gtk.BUTTONS_OK, parentWindow=None):
	messageDialog = gtk.MessageDialog(type=dialogType, buttons=dialogButtons, message_format=message, parent=parentWindow)
	dialogResult = messageDialog.run()
	messageDialog.destroy()
	return dialogResult


# ChildProcessor class
class ChildProcessor:
	def __init__(self):
		self.initInternals()
	
	
	def getExitCodes(self):
		return self.exitCode, self.exitSignal
	
	
	def getStdout(self):
		return self.stdoutBuff
	
	
	def getStderr(self):
		return self.stderrBuff
	
	
	def initInternals(self, command = None):
		self.command = command
		self.exited = False
		self.exitCode = 0
		self.exitSignal = 0
		self.pid = 0
		self.stdin = 0
		self.stdout = 0
		self.stderr = 0
		self.stdoutBuff = ""
		self.stderrBuff = ""
		self.progressDialog = None
		self.progressBar = None
		self.dialogResult = None
		self.readDataCallback = None
	
	
	# This function is called when the child process exits
	def exitCallback(self, pid, condition):
		self.exited = True
		self.exitCode = (condition >> 8) & 0xFF
		self.exitSignal = condition & 0xFF
		logging.debug("ChildProcessor:exitCallback: condition = %s, exitCode = %d, exitSignal = %d" % (condition, self.exitCode, self.exitSignal))
	
	
	# Pulse the progress bar
	def progressPulser(self):
		self.progressBar.pulse()
		return True
	

	# This function is called until the child process exits and there is no more data to read from its stdout/stderr
	def idleCallback(self):
		stdoutBytes = ""
		stderrBytes = ""
		
		try:
			# Check to see if there is data available to be read on child's stdout/stderr
			rds, wrs, ers = select.select([self.stdout, self.stderr], [ ], [self.stdout, self.stderr], 0.0)
			
			if (self.stdout in rds):
				# Process child's stdout
				stdoutBytes = os.read(self.stdout, 256)
				self.stdoutBuff += stdoutBytes
			
			if (self.stderr in rds):
				# Process child's stderr
				stderrBytes = os.read(self.stderr, 256)
				self.stderrBuff += stderrBytes
		except:
			pass
		
		childIsAlive = (self.exited == False or len(stdoutBytes) != 0 or len(stderrBytes) != 0)
		
		# Is there a callback for read data?
		if (self.readDataCallback != None):
			self.readDataCallback(self, childIsAlive)
		
		# If the child has exited and there is no more data to read, then we are done
		if (childIsAlive == False):
			logging.debug("ChildProcessor:idleCallback: no more data to read")
			self.progressBar.set_fraction(1.0)
			self.progressDialog.response(gtk.RESPONSE_ACCEPT)
		
		return childIsAlive
	
	
	def run(self, command = None, dialogTitle = "", dialogMessage = "", pulseProgress = False, readDataCallback = None, errMsg = "A problem occurred while executing the child process:\n", parentWin=None):
		self.initInternals(command)
		
		self.readDataCallback = readDataCallback
		
		progress_event_id = 0
		periodic_event_id = 0
		watch_event_id = 0
		
		try:
			# Start child
			self.pid, self.stdin, self.stdout, self.stderr = gobject.spawn_async(self.command, flags=gobject.SPAWN_DO_NOT_REAP_CHILD, standard_output=True, standard_error=True)
			logging.debug("ChildProcessor:run: spawn_async cmd_id = %d, cmd_stdout = %d, cmd_stderr = %d" % (self.pid, self.stdout, self.stderr))
			
			# Watch for data on the child's stdout/stderr, watch for termination of the child process
			periodic_event_id = gobject.idle_add(self.idleCallback, priority=gobject.PRIORITY_LOW)
			watch_event_id = gobject.child_watch_add(self.pid, self.exitCallback)
			
			if (pulseProgress):
				# Tickle progress bar at a controlled rate (idle task goes too fast)
				progress_event_id = gobject.timeout_add(250, self.progressPulser)
			
			self.progressDialog = gtk.Dialog(dialogTitle, parentWin, gtk.DIALOG_MODAL, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
			self.progressDialog.set_geometry_hints(self.progressDialog, min_width=300)
			self.progressDialog.vbox.pack_start(gtk.Label(dialogMessage), False, False, 5)
			self.progressBar = gtk.ProgressBar()
			self.progressDialog.vbox.pack_start(self.progressBar, True, True, 5)
			self.progressDialog.show_all()
			
			# Now run the dialog while the child executes (the idle callback will trigger close of the dialog when appropriate)
			self.dialogResult = self.progressDialog.run()
			logging.debug("ChildProcessor:run: dialog result = %s" % self.dialogResult)
			
			self.progressDialog.hide()
			
			# Kill the child if user hit cancel
			if (self.dialogResult == gtk.RESPONSE_CANCEL):
				os.kill(self.pid, signal.SIGKILL)
			
		except:
			# Oops-- something bad happened during execution of child process
			exc_type, exc_value, exc_traceback = sys.exc_info()
			logging.info("ChildProcessor:run: caught exception")
			logging.info("ChildProcessor:run: exc_type = " + str(exc_type))
			logging.info("ChildProcessor:run: exc_value = " + str(exc_value))
			logging.info("ChildProcessor:run: exc_traceback = " + str(exc_traceback))
			
			runMessageDialog(errMsg + str(exc_value), parentWindow=parentWin)
		
		
		# Clean up transient objects
		
		if (periodic_event_id): gobject.source_remove(periodic_event_id)
		if (watch_event_id): gobject.source_remove(watch_event_id)
		if (progress_event_id): gobject.source_remove(progress_event_id)
		if (self.stdout): os.close(self.stdout)
		if (self.stderr): os.close(self.stderr)
		
		if (self.progressDialog): self.progressDialog.destroy()
		self.progressDialog = None
		self.progressBar = None
		
		return self.dialogResult



class Convert770:
	def __init__(self, dvdDevice):
		# Path to DVD device
		self.dvdDevicePath = dvdDevice
		
		# Create GUI via glade
		self.xml = gtk.glade.XML('/usr/share/simple770/simple770.glade')
		
		self.window = self.xml.get_widget('main_window')
		self.notebook = self.xml.get_widget('main_notebook')
		self.aviFileEntry = self.xml.get_widget('filepath_text_entry')
		self.aviPreviewButton = self.xml.get_widget('preview_file_button')
		self.dvdTracksComboBox = self.xml.get_widget('dvd_title_combobox')
		self.dvdPreviewButton = self.xml.get_widget('preview_dvd_button')
		self.logTextview = self.xml.get_widget('log_textview')
		self.audioBitrateEntry = self.xml.get_widget('audio_bitrate_entry')
		self.videoBitrateEntry = self.xml.get_widget('video_bitrate_entry')
		self.videoWidthEntry = self.xml.get_widget('video_width_entry')
		self.videoTwoPassCheckbox = self.xml.get_widget('two_pass_checkbox')
		
		self.xml.signal_connect('moof_button_clicked_cb', self.moofCallback)
		self.xml.signal_connect('scan_dvd_button_clicked_cb', self.scanDVDHandler)
		self.xml.signal_connect('preview_dvd_button_clicked_cb', self.previewVideoHandler)
		self.xml.signal_connect('choose_file_button_clicked_cb', self.chooseFileHandler)
		self.xml.signal_connect('preview_file_button_clicked_cb', self.previewVideoHandler)
		self.xml.signal_connect('convert_video_button_clicked_cb', self.convertHandler)
		self.xml.signal_connect('quit_button_clicked_cb', self.quitHandler)
		self.xml.signal_connect('clear_log_button_clicked_cb', self.clearLogCallback)
		
		self.window.connect("delete_event", self.deleteHandler)
		
		self.window.show_all()
		
		self.isFullScreen = False
	
	
	# Convenient place to stick test code
	def moofCallback(self, widget, data=None):
		runMessageDialog("Dogcows go 'Moof'", parentWindow=self.window)
		#~ if (self.isFullScreen):
			#~ self.window.unfullscreen()
			#~ self.isFullScreen = False
		#~ else:
			#~ self.window.fullscreen()
			#~ self.isFullScreen = True
 


	# Clear conversion log
	def clearLogCallback(self, widget, data=None):
		textbuffer = self.logTextview.get_buffer()
		textbuffer.set_text("")
 


	# Callback for "Preview File" buttons
	def previewVideoHandler(self, widget, data=None):
		cmdLine = ["/usr/bin/mplayer", "-really-quiet"]
		
		if (widget == self.aviPreviewButton):
			sourceFilePath = self.aviFileEntry.get_text()
		else:
			dvdTrack = str(self.dvdTracksComboBox.get_active() + 1)
			sourceFilePath = "dvd://" + dvdTrack
			cmdLine.append("-dvd-device")
			cmdLine.append(self.dvdDevicePath)
		
		cmdLine.append(sourceFilePath)
		
		childProcessor = ChildProcessor()
		resultCode = childProcessor.run(cmdLine, "Previewing video", "Playing " + sourceFilePath, pulseProgress=True, parentWin=self.window)
		logging.debug("previewVideoHandler: mplayer resultCode = " + str(resultCode))



	def dumpOutputToLog(self, cmdLine, cmdName, exitCode, exitSignal, stdoutText, stderrText):
		# Dump the text to the log
		textbuffer = self.logTextview.get_buffer()
		iter = textbuffer.get_end_iter()
		textbuffer.insert(iter, "\n*********************************************************************\n")
		textbuffer.insert(iter, "cmdLine = " + str(cmdLine) + "\n")
		textbuffer.insert(iter, cmdName + " exitCode = %d, exitSignal = %d" % (exitCode, exitSignal))
		textbuffer.insert(iter, "\n************************** " + cmdName + " output **************************\n")
		textbuffer.insert(iter, stdoutText);
		textbuffer.insert(iter, "\n************************** " + cmdName + " errors **************************\n")
		textbuffer.insert(iter, stderrText);
		textbuffer.insert(iter, "\n");

	

	# Callback for "Scan DVD" button
	def scanDVDHandler(self, widget, data=None):
		# Clear out the list of DVD tracks
		model = self.dvdTracksComboBox.get_model()
		if (model != None): model.clear()
		
		# Run lsdvd as a child process
		cmdLine = ["/usr/bin/lsdvd", "-v", self.dvdDevicePath ]
		logging.info("scanDVDHandler: cmdLine = %s" % cmdLine)
		
		childProcessor = ChildProcessor()
		dialogResult = childProcessor.run(cmdLine, "Simple770", "DVD scanning in progress....", pulseProgress=True, parentWin=self.window)
		logging.debug("scanDVDHandler: dialog result = %s" % dialogResult)
		
		# Dump the output from lsdvd to the log
		exitCode, exitSignal = childProcessor.getExitCodes()
		stdoutText, stderrText = childProcessor.getStdout(), childProcessor.getStderr()
		self.dumpOutputToLog(cmdLine, "lsdvd", exitCode, exitSignal, stdoutText, stderrText)
		
		# Pop up a dialog if there were problems
		if (exitCode != 0):
			runMessageDialog("An error occurred during DVD scanning (check log panel)", parentWindow=self.window)
			return
		
		# User cancelled?
		if (dialogResult == gtk.RESPONSE_CANCEL):
			return
		
		# Success, so process each line in stdout
		currentDroptext = ""
		
		for line in stdoutText.splitlines():
			words = line.split(" ")
			
			if (words[0].strip() == "Title:"):
				#Title: 01, Length: 01:39:44.066 Chapters: 45, Cells: 59, Audio streams: 04, Subpictures: 04
				trackNum, trackLength = words[0] + " " + words[1][:-1], words[3][:-4]
				currentDroptext = trackNum + " - " + trackLength
				
			elif (words[0].strip() == "VTS:"):
				#VTS: 05, TTN: 01, FPS: 29.97, Format: NTSC, Aspect ratio: 16/9, Width: 720, Height: 480, DF: Letterbox
				fps, format, aspectRatio, vidSize = words[5][:-1], words[7][:-1], words[10][:-1], words[12][:-1] + "x" + words[14][:-1]
				currentDroptext  += " - " + fps + "fps " + format + " " + aspectRatio + " " + vidSize
				self.dvdTracksComboBox.append_text(currentDroptext)
				
			elif (words[0] == "Longest" and words[1] == "track:"):
				# Longest track: 01
				trackNum = int(words[2])
				self.dvdTracksComboBox.set_active(trackNum - 1)


	# Callback for "Quit" button
	def quitHandler(self, widget, data=None):
		gtk.main_quit()
	
	
	# Get the source + destination filepath
	def getSrcDstFilepath(self):
		sourceFilePath = self.aviFileEntry.get_text()
		dvdTrack = self.dvdTracksComboBox.get_active_text()
		
		if (sourceFilePath == None and dvdTrack == None):
			runMessageDialog("No source file or DVD title selected", gtk.MESSAGE_ERROR, parentWindow=self.window)
			return None, None
		
		elif (self.notebook.get_current_page() == 0):
			# Insert a '.770' before the final extension of the filepath (e.g. MyMovie.avi -> MyMovie.770.avi)
			dotIndex = sourceFilePath.rfind('.')
			if (dotIndex < 0): dotIndex = len(sourceFilePath)
			destFilePath = sourceFilePath[:dotIndex] + '.770' + sourceFilePath[dotIndex:]
			
		else:
			dvdTrack = str(self.dvdTracksComboBox.get_active() + 1)
			sourceFilePath = "dvd://" + dvdTrack
			destFilePath = "dvd." + dvdTrack + ".770.avi"
		
		# Extract the filename from the filepath
		destFileDir, destFilename = os.path.split(destFilePath)
		logging.info("getSrcDstFilepath: generated destFilePath = %s, destFileDir = %s, destFilename = %s" % (destFilePath, destFileDir, destFilename))
		
		# Have user confirm the name/location of dest file
		saveFileButtonSet = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)
		saveFileDialog = gtk.FileChooserDialog("Save as....", parent=self.window, action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=saveFileButtonSet)
		saveFileDialog.set_current_folder(destFileDir)
		saveFileDialog.set_current_name(destFilename)
		saveFileDialog.set_default_response(gtk.RESPONSE_OK)
		saveFileDialog.set_do_overwrite_confirmation(True)
		
		response = saveFileDialog.run()
		destFilePath = saveFileDialog.get_filename()
		saveFileDialog.destroy()
		
		if (response == gtk.RESPONSE_CANCEL):
			sourceFilePath, destFilePath = None, None
		
		return sourceFilePath, destFilePath



	# Callback for processing mencoder stdout output
	def mencoderDataReadCallback(self, childProcessor, childIsAlive):
		# Check for a return-delimited line
		front,cr,back = childProcessor.stdoutBuff.partition('\r')
		
		while (cr == '\r'):
			# Extract the return-delimited line, leave linefeed-delimited lines in the buffer
			front,lf,line = front.rpartition('\n')
			childProcessor.stdoutBuff = front + lf + back
			
			# Get next line in preparation for next pass
			front,cr,back = childProcessor.stdoutBuff.partition('\r')
			
			
			# The mencoder output looks like this (fixed char offsets!):
			# Pos:3577.6s  85778f (52%) 67.18fps Trem:  19min 469mb  A-V:0.052 [505:71]
			
			if (line[0:4] != "Pos:" or line[20] != '(' or line[23] != '%' or line[24] != ')'):
				continue
			
			timeSecsRaw = int(line[4:8])
			percent = line[21:23]
			logging.debug("mencoderDataReadCallback: percent = %s, timeSecsRaw = %s" % (percent, timeSecsRaw))
			
			# Only update GUI if the time has changed
			if (self.mencoderTime == timeSecsRaw):
				continue
			
			#self.convertProgressBar.set_text(percent + "% - " + timeStr)
			self.mencoderTime = timeSecsRaw
			timeSecs = timeSecsRaw % 60
			timeMins = (timeSecsRaw / 60) % 60
			timeHours = timeSecsRaw // 3600
			timeStr = "Videoplay Converted: %02d:%02d:%02d" % (timeHours, timeMins, timeSecs)
			childProcessor.progressBar.set_text(timeStr)
			
			newPercentage = (float(percent) + 1.0)/ 100.0
			childProcessor.progressBar.set_fraction(newPercentage)
			
			self.mencoderMaxFrame = int(line[12:18].strip())
	
	
	# Callback for "Convert" button (this is the core of the applet)
	def convertHandler(self, widget, data=None):
		# Figure out the conversion settings
		audioBitrate = self.audioBitrateEntry.get_text()
		videoBitrate = self.videoBitrateEntry.get_text()
		videoResolution = self.videoWidthEntry.get_text()
		
		if (self.videoTwoPassCheckbox.get_active()):
			numPasses = 2
		else:
			numPasses = 1
		
		logging.info("convertHandler: audioBitrate = %s, videoBitrate = %s, videoResolution = %s, numPasses = %d" % (audioBitrate, videoBitrate, videoResolution, numPasses))
		
		sourceFilePath, destFilePath = self.getSrcDstFilepath()
		if (sourceFilePath == None or destFilePath == None):
			return
		
		logging.debug("convertHandler: sourceFilePath = %s, destFilePath = %s" % (sourceFilePath, destFilePath))
		
		for i in range(numPasses):
			# Set up data used by child process callback
			self.mencoderTime = 0.0
			self.mencoderMaxFrame = 0
			
			# lavcopts depends on number of passes
			lavcopts = "vcodec=mpeg4:autoaspect:vbitrate=" + videoBitrate
			lavcMultiPassOpts = [":vpass=1:turbo", ":vpass=2"]
			
			if (numPasses > 1):
				lavcopts += lavcMultiPassOpts[i]
			
			cmdLine = ["/usr/bin/mencoder", sourceFilePath,
						"-o", destFilePath, 
						"-oac",  "mp3lame", "-lameopts", "abr:br=" + audioBitrate, 
						"-ovc", "lavc", "-lavcopts", lavcopts, 
						"-vf", "scale", "-zoom", "-xy", videoResolution,
						"-ffourcc", "DIVX",
						"-idx"]
			
			if (self.notebook.get_current_page() == 1):
				cmdLine.append("-dvd-device")
				cmdLine.append(self.dvdDevicePath)
			
			logging.info("convertHandler: cmdLine = %s" % cmdLine)
			
			startTime = time.time()
			
			textbuffer = self.logTextview.get_buffer()
			iter = textbuffer.get_end_iter()
			textbuffer.insert(iter, "\n*********************************************************************\n")
			textbuffer.insert(iter, "executing command: " + str(cmdLine) + "\n")
			textbuffer.insert(iter, "mencoder execution start time = " + time.asctime(time.localtime()) + "\n")
			logging.info("convertHandler: execution start time = " + time.asctime(time.localtime()))
			
			childProcessor = ChildProcessor()
			dialogResult = childProcessor.run(command = cmdLine, dialogTitle = "Simple770", dialogMessage = "Converting video - Pass #" + str(i + 1), readDataCallback = self.mencoderDataReadCallback, parentWin=self.window)
			logging.debug("convertHandler: dialog result = %s" % dialogResult)
			
			
			# Dump the output from mencoder to the log
			exitCode, exitSignal = childProcessor.getExitCodes()
			stdoutText, stderrText = childProcessor.getStdout(), childProcessor.getStderr()
			endTime = time.time()
			deltaTime = endTime - startTime
			frameRate = float(self.mencoderMaxFrame) / deltaTime
			textbuffer.insert(iter, "mencoder execution end time = " + time.asctime(time.localtime()) + "\n")
			logging.info("convertHandler: execution end time = " + time.asctime(time.localtime()))
			textbuffer.insert(iter, "mencoder frames processed = %d, time = %d seconds, framerate = %.2ffps\n" % (self.mencoderMaxFrame, int(deltaTime), frameRate))
			logging.info("convertHandler: mencoder frames processed = %d, time = %d seconds, framerate = %.2ffps\n" % (self.mencoderMaxFrame, int(deltaTime), frameRate))
			self.dumpOutputToLog(cmdLine, "mencoder", exitCode, exitSignal, stdoutText, stderrText)
			
			if (dialogResult == gtk.RESPONSE_CANCEL or exitCode != 0):
				break
			
			
		# Pop up a dialog to let the user know it's all done....
		if (dialogResult == gtk.RESPONSE_CANCEL):
			runMessageDialog("Conversion cancelled by user", parentWindow=self.window)
		elif (exitCode != 0):
			runMessageDialog("An error occurred during conversion (check log panel)", parentWindow=self.window)
		else:
			runMessageDialog("Conversion complete", parentWindow=self.window)
	
	
	
	# Let the user choose an AVI file, then store the file name in the aviFile Entry
	def chooseFileHandler(self, widget, data=None):
		fileChooserButtonSet = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)
		fileChooser = gtk.FileChooserDialog("Select file to convert", self.window, gtk.FILE_CHOOSER_ACTION_OPEN, fileChooserButtonSet)
		fileChooser.set_default_response(gtk.RESPONSE_OK)
		
		dialogResponse = fileChooser.run()
		
		if dialogResponse == gtk.RESPONSE_OK:
			chosenFile = fileChooser.get_filename()
			logging.debug("chooseFileHandler: Selected file '%s'" % chosenFile)
			self.aviFileEntry.set_text(chosenFile)
		
		fileChooser.destroy()
	
	
	# Handle the Gtk delete event when the user tries to close this app
	def deleteHandler(self, widget, event, data=None):
		gtk.main_quit()
		return False  # False means okay to delete app
		

	# Nothing to do here but pass on through to gtk.main()
	def main(self):
		gtk.main()


if __name__ == "__main__":
	parser = optparse.OptionParser(version=myVersion)
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="generate verbose output to stdout")
	parser.add_option("-d", "--device", action="store", dest="dvdDevice", default="/dev/dvd", help="specify the DVD device")
	(options, args) = parser.parse_args()
	
	#options.verbose = True
	
	if (options.verbose):
		logging.basicConfig(level=logging.DEBUG)
	else:
		logging.basicConfig(level=logging.INFO)
	
	logging.debug("main: command line = " + str(sys.argv))
	
	
	converter = Convert770(options.dvdDevice)
	converter.main()
